﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Web;

namespace OAuth
{
    public class OAuthHttpRequest
    {
        // http://oauth.googlecode.com/svn/code/csharp/OAuthBase.cs/
        private readonly static OAuthBase oAuth = new OAuthBase();

        private const string OAuthHeaderSignaturePattern = "OAuth oauth_consumer_key=\"{0}\", oauth_token=\"{1}\", oauth_signature_method=\"HMAC-SHA1\", oauth_signature=\"{2}\", oauth_timestamp=\"{3}\", oauth_nonce=\"{4}\", oauth_version=\"1.0\"";
        private const string OAuthUriSignaturePattern = "oauth_consumer_key={0}&oauth_token={1}&oauth_signature_method=HMAC-SHA1&oauth_signature={2}&oauth_timestamp={3}&oauth_nonce={4}&oauth_version=1.0";

        // http://en.wikipedia.org/wiki/Percent-encoding
        private const string ReservedCharacters = @"`!@#$%^&*()=+[]{}\|';:,<>/?"; // " will be encoded automatically

        private NameValueCollection parameters = new NameValueCollection(StringComparer.OrdinalIgnoreCase);
        private Dictionary<string, object> postDataNoSign = new Dictionary<string,object>();
        private byte[] body;

        private string baseUrl;
        private string queryStringOAuthWithNoSignature;
        private string queryStringRequestParameter;
        private string signature;
        private string nonce;
        private string timeStamp;

        private HttpWebRequest request;

        internal OAuthHttpRequest() { }

        private void ValidateParameter()
        {
            ParamValidator.ValidateNull(RequestUrl, "RequestUrl");

            if (ProtocolParameterTransmission < OAuthParameterTransmission.AuthorizationHeader ||
                ProtocolParameterTransmission > OAuthParameterTransmission.RequestUriQuery)
            {
                throw new ArgumentException("ParameterTransmission has invalid value");
            }

            if (RequestParameterSource < OAuthRequestParameterSource.RequestUriQuery ||
                RequestParameterSource > OAuthRequestParameterSource.MultiPartForm)
            {
                throw new ArgumentException("ParameterLocation has invalid value");
            }

            if (!HttpMethod.Equals(WebRequestMethods.Http.Get) && !HttpMethod.Equals(WebRequestMethods.Http.Post))
            {
                throw new ArgumentException("Method invalid or unsupported");
            }

            if (ProtocolParameterTransmission == OAuthParameterTransmission.FormEncodedBody &&
                RequestParameterSource == OAuthRequestParameterSource.MultiPartForm ||
                ProtocolParameterTransmission == OAuthParameterTransmission.FormEncodedBody &&
                !HttpMethod.Equals(WebRequestMethods.Http.Post, StringComparison.OrdinalIgnoreCase) ||
                RequestParameterSource != OAuthRequestParameterSource.RequestUriQuery &&
                !HttpMethod.Equals(WebRequestMethods.Http.Post, StringComparison.OrdinalIgnoreCase))
            {
                throw new ArgumentException("Combination of ParameterTransmission, ParameterSource and HttpMethod are invalid");
            }
        }

        private static string EncodeSymbol(string text)
        {
            StringBuilder result = new StringBuilder();

            bool lowSurrogateNext = false;

            foreach (var ch in text)
            {
                if (lowSurrogateNext)
                {
                    lowSurrogateNext = false;
                    result.Append(ch);
                }
                else if (ReservedCharacters.IndexOf(ch) == -1)
                {
                    if (char.IsHighSurrogate(ch))
                    {
                        lowSurrogateNext = true;
                    }
                    result.Append(ch);
                }
                else
                {
                    result.Append('%' + String.Format(CultureInfo.InvariantCulture, "{0:X2}", (int)ch));
                }
            }

            return result.ToString();
        }

        private string CollectRequestParameter()
        {
            List<string> query = new List<string>(RequestParameter.Count);

            foreach (string key in RequestParameter.Keys)
            {
                query.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", EncodeSymbol(key), EncodeSymbol(RequestParameter[key])));
            }

            return string.Format(CultureInfo.InvariantCulture, "?{0}", string.Join("&", query));
        }

        private string GetOAuthQueryStringWithSignature()
        {
            return string.Format(CultureInfo.InvariantCulture,
                        "{0}&oauth_signature={1}",
                        this.queryStringOAuthWithNoSignature,
                        HttpUtility.UrlEncode(this.signature));
        }

        private string GetOAuthStringWithSignature()
        {
            return string.Format(CultureInfo.InvariantCulture,
                        OAuthUriSignaturePattern,
                        AppKey,
                        Token,
                        HttpUtility.UrlEncode(this.signature),
                        this.timeStamp,
                        this.nonce);
        }

        private static string RemoveStartingQuestionMark(string s)
        {
            if (s.StartsWith("?"))
            {
                s = s.Substring(1);
            }

            return s;
        }

        private string GetFinalUrlFromQueryString(string queryString)
        {
            if (queryString.StartsWith("?"))
            {
                return this.baseUrl + queryString;
            }
            else
            {
                return string.Format(CultureInfo.InvariantCulture, "{0}?{1}", this.baseUrl, queryString);
            }
        }

        private void Sign()
        {
            // Query string in RequestUrl is ignored automatically
            Uri url = new Uri(RequestUrl, CollectRequestParameter());

            this.queryStringRequestParameter = url.Query;

            this.nonce = oAuth.GenerateNonce();
            this.timeStamp = oAuth.GenerateTimeStamp();

            this.signature = oAuth.GenerateSignature(url,
                                          AppKey,
                                          AppSecret,
                                          Token,
                                          TokenSecret,
                                          HttpMethod,
                                          timeStamp,
                                          nonce,
                                          out this.baseUrl,
                                          out this.queryStringOAuthWithNoSignature);
        }

        private static void BuildMultiPartFormDataString(BinaryWriter writer, string header, string name, string value)
        {
            if (value.Length > 0)
            {
                var contents = new StringBuilder();
                contents.AppendLine(header);
                contents.AppendLine(String.Format(CultureInfo.InvariantCulture, "Content-Disposition: form-data; name=\"{0}\"", name));
                contents.AppendLine("Content-Type: text/plain; charset=UTF-8");
                contents.AppendLine("Content-Transfer-Encoding: 8bit");
                contents.AppendLine();
                contents.AppendLine(value);
                writer.Write(Encoding.UTF8.GetBytes(contents.ToString()));
            }
        }

        private static void BuildMultiPartFormDataStream(BinaryWriter writer, string header, string name, byte[] value)
        {
            if (value.Length > 0)
            {
                var contents = new StringBuilder();
                contents.AppendLine(header);
                contents.AppendLine(string.Format(CultureInfo.InvariantCulture, "Content-Disposition: form-data; name=\"{0}\";", name));
                contents.AppendLine("Content-Type: application/octet-stream; charset=UTF-8");
                contents.AppendLine("Content-Transfer-Encoding: binary");
                contents.AppendLine();
                writer.Write(Encoding.UTF8.GetBytes(contents.ToString()));
                writer.Write(value);
                writer.Write((byte)13);
                writer.Write((byte)10);
            }
        }

        private void BuildMultiPartFormData()
        {
            string boundary = Guid.NewGuid().ToString();
            string header = string.Format("--{0}", boundary);
            string footer = string.Format("--{0}--", boundary);

            var ms = new MemoryStream();
            var writer = new BinaryWriter(ms);

            foreach (string key in this.parameters)
            {
                if (!string.IsNullOrEmpty(key))
                {
                    BuildMultiPartFormDataString(writer, header, key, this.parameters[key]);
                }
            }

            foreach (var pair in this.postDataNoSign)
            {
                if (!string.IsNullOrEmpty(pair.Key))
                {
                    if (pair.Value is string)
                    {
                        BuildMultiPartFormDataString(writer, header, pair.Key, (string)pair.Value);
                    }
                    else if (pair.Value is byte[])
                    {
                        BuildMultiPartFormDataStream(writer, header, pair.Key, (byte[])pair.Value);
                    }
                }
            }

            writer.Write(Encoding.UTF8.GetBytes(footer));
            writer.Write((byte)13);
            writer.Write((byte)10);

            this.body = ms.ToArray();
            this.request.ContentType = string.Format(CultureInfo.InvariantCulture, "multipart/form-data; boundary={0}", boundary);
            this.request.ContentLength = this.body.Length;
        }

        private string GetFinalRequestUrl()
        {
            if (RequestParameterSource == OAuthRequestParameterSource.RequestUriQuery)
            {
                if (ProtocolParameterTransmission == OAuthParameterTransmission.RequestUriQuery)
                {
                    return GetFinalUrlFromQueryString(GetOAuthQueryStringWithSignature());
                }
                else
                {
                    return GetFinalUrlFromQueryString(this.queryStringRequestParameter);
                }
            }
            else if (ProtocolParameterTransmission == OAuthParameterTransmission.RequestUriQuery)
            {
                return GetFinalUrlFromQueryString(GetOAuthStringWithSignature());
            }
            else
            {
                return this.baseUrl;
            }
        }

        private void BuildRequestHeaderAndSettings()
        {
            if (ProtocolParameterTransmission == OAuthParameterTransmission.AuthorizationHeader)
            {
                this.request.Headers.Add("Authorization", string.Format(
                    CultureInfo.InvariantCulture,
                    OAuthHeaderSignaturePattern,
                    AppKey,
                    Token,
                    HttpUtility.UrlEncode(this.signature),
                    this.timeStamp,
                    this.nonce));
            }

            this.request.ServicePoint.Expect100Continue = false;
            this.request.Method = HttpMethod;
        }

        private void BuildRequestBody()
        {
            if (ProtocolParameterTransmission == OAuthParameterTransmission.FormEncodedBody)
            {
                this.request.ContentType = "application/x-www-form-urlencoded";

                if (RequestParameterSource == OAuthRequestParameterSource.UrlEncodedForm)
                {
                    this.body = Encoding.ASCII.GetBytes(GetOAuthQueryStringWithSignature());
                }
                else
                {
                    this.body = Encoding.ASCII.GetBytes(GetOAuthStringWithSignature());
                }
            }
            else if (RequestParameterSource == OAuthRequestParameterSource.UrlEncodedForm)
            {
                this.request.ContentType = "application/x-www-form-urlencoded";
                this.body = Encoding.ASCII.GetBytes(RemoveStartingQuestionMark(this.queryStringRequestParameter));
            }
            else if (RequestParameterSource == OAuthRequestParameterSource.MultiPartForm)
            {
                BuildMultiPartFormData();
            }
        }

        private HttpWebRequest CreateRequestIfNecessary()
        {
            if (this.request == null)
            {
                ValidateParameter();

                Sign();

                var url = GetFinalRequestUrl();

                this.request = (HttpWebRequest)WebRequest.Create(url);

                BuildRequestHeaderAndSettings();

                BuildRequestBody();
            }

            return this.request;
        }

        public string AppKey { get; internal set; }

        public string AppSecret { get; internal set; }

        public string Token { get; set; }

        public string TokenSecret { get; set; }

        public NameValueCollection RequestParameter { get { return this.parameters; } }

        public Dictionary<string, object> RequestParameterNoSign { get { return this.postDataNoSign; } }

        public OAuthRequestParameterSource RequestParameterSource { get; set; }

        public OAuthParameterTransmission ProtocolParameterTransmission { get; set; }

        public Uri RequestUrl { get; internal set; }

        public string HttpMethod { get; internal set; }

        public HttpWebRequest UnderlyingRequest { get { return CreateRequestIfNecessary(); } }
    }
}
